/*
! Clone an object
      clone(x)
  Can clone any primitive type, array, and object.
  If x has a function clone, this function will be invoked to clone the object.
  >param {} x
  >return {} clone
*/
export function clone( x ) {
  const type = typeof x;
  // immutable primitive types
  if (
    type === "number" ||
    type === "string" ||
    type === "boolean" ||
    x === null ||
    x === undefined
  ) {
    return x;
  }
  // use clone function of the object when available
  if ( typeof x.clone === "function" ) {
    return x.clone();
  }
  // array
  if ( Array.isArray( x ) ) {
    return x.map( function ( value ) {
      return clone( value );
    } );
  }
  if ( x instanceof Date ) return new Date( x.valueOf() );
  // object
  return mapObject( x, clone );
}
/*
! Apply map to all properties of an object
  >param {Object} object
  >param {function} callback
  >return {Object} Returns a copy of the object with mapped properties
*/
export function mapObject( object, callback ) {
  const clone = {};
  for ( const key in object ) {
    if ( hasOwnProperty( object, key ) ) {
      clone[ key ] = callback( object[ key ] );
    }
  }
  return clone;
}
/*
! Extend object a with the properties of object b
  >param {Object} a
  >param {Object} b
  >return {Object} a
*/
export function extend( a, b ) {
  for ( const prop in b ) {
    if ( hasOwnProperty( b, prop ) ) {
      a[ prop ] = b[ prop ];
    }
  }
  return a;
}
/*
! Deep test equality of all fields in two pairs of arrays or objects.
  Compares values and functions strictly (ie. 2 is not the same as '2').
  >param {Array | Object} a
  >param {Array | Object} b
  >returns {boolean}
*/
export function deepStrictEqual( a, b ) {
  let prop, i, len;
  if ( Array.isArray( a ) ) {
    if ( !Array.isArray( b ) ) {
      return false;
    }
    if ( a.length !== b.length ) {
      return false;
    }
    for ( i = 0, len = a.length; i < len; i++ ) {
      if ( !deepStrictEqual( a[ i ], b[ i ] ) ) {
        return false;
      }
    }
    return true;
  } else if ( typeof a === "function" ) {
    return a === b;
  } else if ( a instanceof Object ) {
    if ( Array.isArray( b ) || !( b instanceof Object ) ) {
      return false;
    }
    for ( prop in a ) {
      // noinspection JSUnfilteredForInLoop
      if ( !( prop in b ) || !deepStrictEqual( a[ prop ], b[ prop ] ) ) {
        return false;
      }
    }
    for ( prop in b ) {
      // noinspection JSUnfilteredForInLoop
      if ( !( prop in a ) || !deepStrictEqual( a[ prop ], b[ prop ] ) ) {
        return false;
      }
    }
    return true;
  } else {
    return a === b;
  }
}
/*
! Recursively flatten a nested object.
  >param {Object} nestedObject
  >return {Object} Returns the flattened object
*/
export function deepFlatten( nestedObject ) {
  const flattenedObject = {};
  _deepFlatten( nestedObject, flattenedObject );
  return flattenedObject;
}
// helper function used by deepFlatten
function _deepFlatten( nestedObject, flattenedObject ) {
  for ( const prop in nestedObject ) {
    if ( hasOwnProperty( nestedObject, prop ) ) {
      const value = nestedObject[ prop ];
      if ( typeof value === "object" && value !== null ) {
        _deepFlatten( value, flattenedObject );
      } else {
        flattenedObject[ prop ] = value;
      }
    }
  }
}
/*
! Test whether the current JavaScript engine supports Object.defineProperty
  >returns {boolean} returns true if supported
*/
export function canDefineProperty() {
  // test needed for broken IE8 implementation
  try {
    if ( Object.defineProperty ) {
      Object.defineProperty( {}, "x", {
        get: function () {}
      } );
      return true;
    }
  } catch ( e ) {}
  return false;
}
/*
! Attach a lazy loading property to a constant.
  The given function `fn` is called once when the property is first requested.
  >param {Object} object         Object where to add the property
  >param {string} prop           Property name
  >param {Function} valueResolver Function returning the property value. Called
                                 without arguments.
*/
export function lazy( object, prop, valueResolver ) {
  let _uninitialized = true;
  let _value;
  Object.defineProperty( object, prop, {
    get: function () {
      if ( _uninitialized ) {
        _value = valueResolver();
        _uninitialized = false;
      }
      return _value;
    },
    set: function ( value ) {
      _value = value;
      _uninitialized = false;
    },
    configurable: true,
    enumerable: true,
  } );
}
/*
! Get a nested property from an object
  >param {Object} object
  >param {string | string[]} path
  >returns {Object}
*/
export function get( object, path ) {
  if ( typeof path === "string" ) {
    if ( isPath( path ) ) {
      return get( object, path.split( "." ) );
    } else {
      return object[ path ];
    }
  }
  let child = object;
  for ( let i = 0; i < path.length; i++ ) {
    const key = path[ i ];
    child = child ? child[ key ] : undefined;
  }
  return child;
}
/*
! Set a nested property in an object
  Mutates the object itself
  If the path doesn't exist, it will be created
  >param {Object} object
  >param {string | string[]} path
  >param {} value
  >returns {Object}
*/
export function set( object, path, value ) {
  if ( typeof path === "string" ) {
    if ( isPath( path ) ) {
      return set( object, path.split( "." ), value );
    } else {
      object[ path ] = value;
      return object;
    }
  }
  let child = object;
  for ( let i = 0; i < path.length - 1; i++ ) {
    const key = path[ i ];
    if ( child[ key ] === undefined ) {
      child[ key ] = {};
    }
    child = child[ key ];
  }
  if ( path.length > 0 ) {
    const lastKey = path[ path.length - 1 ];
    child[ lastKey ] = value;
  }
  return object;
}
